package com.black.utils.encrypt.sm2;

import com.black.utils.converter.ByteConverter;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.gm.GMNamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.Strings;

import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

/**
 * SM2工具类
 * <p>
 * 生成随机的秘钥对
 * 同一明文同一公钥多次加密后,密文不同，
 * 私钥解密结果相同
 * Text => Hex
 */
@Slf4j
public class SM2Util {

    public static final String SM2_DEFAULT_CURVE_NAME = "sm2p256v1";
    public static final String SHA1PRNG = "SHA1PRNG";
    public static final String DEFAULT_ID = "1234567812345678";

    /**
     * SM2加密算法
     *
     * @param publicKey 公钥
     * @param data      待加密的数据
     * @return 密文，BC库产生的密文带由04标识符，与非BC库对接时需要去掉开头的04
     */
    public static String encrypt(String publicKey, String data) {
        // 按国密排序标准加密
        return encrypt(publicKey, data, SM2EngineExtend.CIPHER_MODE_BC);
    }

    /**
     * SM2加密算法
     *
     * @param publicKey  公钥
     * @param data       待加密的数据
     * @param cipherMode 密文排列方式0-C1C2C3；1-C1C3C2；
     * @return 密文，BC库产生的密文带由04标识符，与非BC库对接时需要去掉开头的04
     */
    public static String encrypt(String publicKey, String data, int cipherMode) {
        // 获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName(SM2_DEFAULT_CURVE_NAME);
        // 构造ECC算法参数，曲线方程、椭圆曲线G点、大整数N
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        //提取公钥点
        ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(ByteConverter.hexStringToBytes(publicKey));
        // 公钥前面的02或者03表示是压缩公钥，04表示未压缩公钥, 04的时候，可以去掉前面的04
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);

        SM2EngineExtend sm2Engine = new SM2EngineExtend();
        // 设置sm2为加密模式
        sm2Engine.init(true, cipherMode, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));

        try {
            byte[] in = data.getBytes();
            byte[] arrayOfBytes = sm2Engine.processBlock(in, 0, in.length);
            return ByteConverter.bytesToHexString(arrayOfBytes);
        } catch (Exception e) {
            log.error("SM2加密时出现异常:{}", e.getMessage());
        }
        return null;
    }

    /**
     * 重载方法 随机生成一对秘钥
     * 单次使用不可重复调用
     * 默认不压缩公钥
     *
     * @return 密钥对
     */
    public static SM2KeyPair generateKeyPair() {
        return generateKeyPair(false);
    }

    /**
     * 获取sm2密钥对
     * BC库使用的公钥=64个字节+1个字节（04标志位），BC库使用的私钥=32个字节
     * SM2秘钥的组成部分有 私钥D 、公钥X 、 公钥Y , 他们都可以用长度为64的16进制的HEX串表示，
     * <br/>SM2公钥并不是直接由X+Y表示 , 而是额外添加了一个头，当启用压缩时:公钥=有头+公钥X ，即省略了公钥Y的部分
     *
     * @param compressed 是否压缩公钥（加密解密都使用BC库才能使用压缩）
     * @return SM2密钥对，包含公钥和私钥
     */
    public static SM2KeyPair generateKeyPair(boolean compressed) {
        //获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName(SM2_DEFAULT_CURVE_NAME);
        //构造domain参数
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
        //1.创建密钥生成器
        ECKeyPairGenerator keyPairGenerator = new ECKeyPairGenerator();
        //2.初始化生成器,带上随机数
        try {
            keyPairGenerator.init(new ECKeyGenerationParameters(domainParameters, SecureRandom.getInstance(SHA1PRNG)));
        } catch (NoSuchAlgorithmException e) {
            log.error("生成公私钥对时出现异常:{}", e.getMessage());
        }
        //3.生成密钥对
        AsymmetricCipherKeyPair asymmetricCipherKeyPair = keyPairGenerator.generateKeyPair();
        ECPublicKeyParameters publicKeyParameters = (ECPublicKeyParameters) asymmetricCipherKeyPair.getPublic();
        ECPoint ecPoint = publicKeyParameters.getQ();
        // 把公钥放入map中,默认压缩公钥
        // 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥,04的时候,可以去掉前面的04
        String publicKey = ByteConverter.bytesToHexString(ecPoint.getEncoded(compressed));
        ECPrivateKeyParameters privateKeyParameters = (ECPrivateKeyParameters) asymmetricCipherKeyPair.getPrivate();
        BigInteger intPrivateKey = privateKeyParameters.getD();
        // 把私钥放入map中
        String privateKey = intPrivateKey.toString(16);
        return new SM2KeyPair(publicKey, privateKey);
    }

    /**
     * SM2解密算法
     *
     * @param privateKey 私钥
     * @param cipherData 密文数据
     * @return 解密后的明文数据
     */
    public static String decrypt(String privateKey, String cipherData) {
        // 按国密排序标准解密
        return decrypt(privateKey, cipherData, SM2EngineExtend.CIPHER_MODE_BC);
    }

    /**
     * SM2解密算法
     *
     * @param privateKey 私钥
     * @param cipherData 密文数据
     * @param cipherMode 密文排列方式0-C1C2C3；1-C1C3C2；
     * @return 解密后的明文数据
     */
    public static String decrypt(String privateKey, String cipherData, int cipherMode) {
        // 使用BC库加解密时密文以04开头，传入的密文前面没有04则补上
        if (!cipherData.startsWith("04")) {
            cipherData = "04" + cipherData;
        }
        byte[] cipherDataByte = ByteConverter.hexStringToBytes(cipherData);

        //获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName(SM2_DEFAULT_CURVE_NAME);
        //构造domain参数
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());

        BigInteger privateKeyD = new BigInteger(privateKey, 16);
        ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);

        SM2EngineExtend sm2Engine = new SM2EngineExtend();
        // 设置sm2为解密模式
        sm2Engine.init(false, cipherMode, privateKeyParameters);

        String result = "";
        try {
            byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
            return new String(arrayOfBytes);
        } catch (Exception e) {
            log.error("SM2解密时出现异常:{}", e.getMessage());
        }
        return result;
    }

    /**
     * SM2签名
     *
     * @param privateKey 秘钥
     * @param data       待签名内容
     * @return 数字签名
     */
    public static String sign(String privateKey, String data) {
        //待签名内容转为字节数组
        byte[] message = data.getBytes();
        //获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName(SM2_DEFAULT_CURVE_NAME);
        //构造domain参数
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(),
                sm2ECParameters.getG(), sm2ECParameters.getN());
        BigInteger privateKeyD = new BigInteger(privateKey, 16);
        ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);
        //创建签名实例
        SM2Signer sm2Signer = new SM2Signer();
        //初始化签名实例,带上ID,国密的要求,ID默认值:1234567812345678
        try {
            sm2Signer.init(true, new ParametersWithID(new ParametersWithRandom(privateKeyParameters, SecureRandom.getInstance(SHA1PRNG)), Strings.toByteArray(DEFAULT_ID)));
            sm2Signer.update(message, 0, message.length);
            byte[] signBytes = sm2Signer.generateSignature();
            return ByteConverter.bytesToHexString(signBytes);
        } catch (NoSuchAlgorithmException | CryptoException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * SM2验签
     *
     * @param publicKey 公钥
     * @param data      待验签内容
     * @param sign      签名
     * @return 验签结果
     */
    public static boolean verify(String publicKey, String data, String sign) {
        //待签名内容
        byte[] message = data.getBytes();
        byte[] signData = ByteConverter.hexStringToBytes(sign);
        // 获取一条SM2曲线参数
        X9ECParameters sm2ECParameters = GMNamedCurves.getByName(SM2_DEFAULT_CURVE_NAME);
        // 构造domain参数
        ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(),
                sm2ECParameters.getG(),
                sm2ECParameters.getN());
        //提取公钥点
        ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(ByteConverter.hexStringToBytes(publicKey));
        // 公钥前面的02或者03表示是压缩公钥，04表示未压缩公钥, 04的时候，可以去掉前面的04
        ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
        //创建签名实例
        SM2Signer sm2Signer = new SM2Signer();
        ParametersWithID parametersWithID = new ParametersWithID(publicKeyParameters, Strings.toByteArray(DEFAULT_ID));
        sm2Signer.init(false, parametersWithID);
        sm2Signer.update(message, 0, message.length);
        //验证签名结果
        return sm2Signer.verifySignature(signData);
    }
}




